<?PHP
#
#   FILE:  Chart_Base.php
#
#   Part of the Metavus digital collections platform
#   Copyright 2017-2020 Edward Almasy and Internet Scout Research Group
#   http://metavus.net
#

namespace Metavus;

use Exception;
use InvalidArgumentException;

/**
* Base class for generating and displaying a chart.
*/
abstract class Chart_Base
{
    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
    * Get/set data for chart.
    * @param array $NewValue Data in format specified by chart type.(OPTIONAL)
    * @return array Current data.
    */
    public function data($NewValue = null)
    {
        if ($NewValue !== null) {
            $this->Data = $NewValue;
        }
        return $this->Data;
    }

    /**
     * Set legend position.
     * @param string $Position Legend position as a BarChart::LEGEND_
     *     constant.LEGEND_BOTTOM will place the legend below the chart,
     *     LEGEND_RIGHT will place it at the right, LEGEND_INSET will display
     *     it as an inset within the graphic, and LEGEND_NONE will disable the
     *     legend entirely.
     * @throws Exception If an invalid legend position is supplied.
     */
    public function legendPosition($Position)
    {
        if (!in_array($Position, [
            self::LEGEND_BOTTOM,
            self::LEGEND_RIGHT,
            self::LEGEND_INSET,
            self::LEGEND_NONE
        ])) {
            throw new Exception("Unsupported legend position: ".$Position);
        }
        $this->LegendPosition = $Position;
    }

    /**
     * Set shortened labels to be used in the legend of the chart.
     * @param array $LegendLabels Array of legend labels to use, with keys
     *     matching the keys of the data passed to the constructor.Any labels
     *     that do not need shortening may be omitted.
     */
    public function legendLabels($LegendLabels)
    {
        $this->LegendLabels = $LegendLabels;
    }

    /**
     * Get/set chart element labels (pie slices, bars, etc).
     * @param array|boolean $NewValue Label strings for chart, indexed by the same
     *       values used to index chart data, or FALSE to clear any existing
     *       labels.(OPTIONAL)
     * @return array|boolean $Labels Label strings for chart, indexed by the same
     *       values used to index chart data, or FALSE if no labels have been set.
     * @throws InvalidArgumentException If invalid type of new value supplied.
    */
    public function labels($NewValue = null)
    {
        if ($NewValue !== null) {
            if (!is_array($NewValue) && ($NewValue !== false)) {
                throw new InvalidArgumentException("Invalid new value (".$NewValue.").");
            }
            $this->Labels = $NewValue;
        }
        return $this->Labels;
    }

    /**
    * Get/set color palette.Bars with no color defined will use an
    * autogenerated color.
    * @param array|false $NewValue Array of hex colors (e.g., '#1f77b4'),
    *       indexed by the keys used in the constructor, or false to clear
    *       any existing color palette.(OPTIONAL).
    * @return array|false Current color palette, or false if no color palette set.
    */
    public function colors($NewValue = null)
    {
        if ($NewValue !== null) {
            $this->Colors = $NewValue;
        }
        return $this->Colors;
    }

    /**
    * Get/Set height of the chart including the legend.
    * @param int $NewValue New height in pixels (OPTIONAL).
    * @return int Current height in pixels.
    */
    public function height(int $NewValue = null)
    {
        if ($NewValue !== null) {
            $this->Height = $NewValue;
        }
        return $this->Height;
    }

    /**
    * Get/Set width of the chart including the legend.
    * @param int $NewValue New width in pixels (OPTIONAL).
    * @return int Current width in pixels.
    */
    public function width(int $NewValue = null)
    {
        if ($NewValue !== null) {
            $this->Width = $NewValue;
        }
        return $this->Width;
    }

    /**
    * Display a chart.These charts are currently generated with
    * c3.js, which outputs SVG that is annotated with CSS classes for
    * styling (C3 is only relevant insofar as it is reflected in the
    * CSS class names).For example, if there is a data item called
    * "Engineering", then the chart element for that item will be in a
    * g.c3-target-Engineering.The text giving the label for that
    * element can be changed to black with CSS like:
    *
    * g.c3-target-Engineering text {
    *   fill: black;
    * }
    *
    * Similarly, the text for the corresponding legend item:
    *
    * g.c3-legend-item-Engineering text {
    *   fill: green;
    * }
    *
    * Note that the foreground color of an SVG text is controlled by
    * 'fill' rather than 'color' because consistency is overrated.A
    * reference to the SVG elements and the CSS properties that they
    * understand can be found at:
    *
    *   https://developer.mozilla.org/en-US/docs/Web/SVG/Element
    *
    * In practice, it's often simplest to use a browser's "Inspect
    * Element" feature to locate SVG elements in order to customize
    * them.
    *
    * @param string $ContainerId Id to use when generating the div to contain this chart.
    */
    public function display(string $ContainerId)
    {
        self::requireNeededFiles();

        # declare the chart data that we will give to c3.generate
        # for function callbacks, give the function a name ending with
        # '_fn' and include the function name as a string
        $this->Chart = [
            "bindto" => "#".$ContainerId,
            "size" => [
                "height" => $this->Height,
                "width" => $this->Width,
            ],
            "tooltip" => [
                "format" => [
                    "name" => "tooltip_name_fn",
                ],
            ],
        ];

        # set up legend positioning
        if ($this->LegendPosition == self::LEGEND_NONE) {
            $this->Chart["legend"]["show"] = false;
        } else {
            $this->Chart["legend"]["position"] = $this->LegendPosition;
        }

        # if the user provided a color palette, set that up as will
        if ($this->Colors) {
            # sort user-provided colors into the correct order
            $Palette = [];

            # if this is a 2d array
            if (is_array(reset($this->Data))) {
                # iterate over the inner dimension to generate the palette
                foreach ($this->Data as $Index => $Values) {
                    foreach ($Values as $VIndex => $Value) {
                        $Palette[] = isset($this->Colors[$VIndex]) ?
                                  $this->Colors[$VIndex] :
                                  $this->generateRgbColorString($VIndex);
                    }
                }
            } else {
                # otherwise, iterate over the outer dimension
                foreach ($this->Data as $Index => $Value) {
                    $Palette[] = isset($this->Colors[$Index]) ?
                              $this->Colors[$Index] :
                              $this->generateRgbColorString($Index);
                }
            }
            $this->Chart["color"]["pattern"] = $Palette;
        }

        static::prepareData();

        // @codingStandardsIgnoreStart
        ?><div id="<?= $ContainerId ?>" class="mv-<?= strtolower(get_called_class()) ?>"></div>
        <script type="text/javascript">
        $(document).ready(function(){
            // define state variables for this chart
            var chart;
            var label_lut = <?= json_encode($this->TooltipLabels) ?>;
            <?= $this->StateVariableJSCode; ?>

            // define helper functions for this chart
            function tooltip_name_fn(name, ratio, id, index) {
                return name in label_lut ? label_lut[name] : name;
            }
            <?= $this->HelperFunctionJSCode; ?>

            // get the chart spec data
            var chart_spec = <?= json_encode($this->Chart) ?>;

            // convert any strings that refer to functions into callable function refs
            function eval_fns(obj){
                for (var prop in obj) {
                    if (typeof obj[prop] == "object") {
                        eval_fns(obj[prop]);
                    } else if (typeof obj[prop] == "string" && obj[prop].match(/_fn$/)) {
                        obj[prop] = eval(obj[prop]);
                    }
                }
            }

            eval_fns(chart_spec);

            // generate the chart
            chart = c3.generate(chart_spec);

            <?= $this->PostChartInitJSCode; ?>

            $("#<?= $ContainerId ?>").data('c3-chart', chart);
        });
        </script><?PHP
        // @codingStandardsIgnoreEnd
    }

    /**
    * Set up the chart environment so that cached HTML from a previous
    * Display() call will function properly.
    */
    public static function prepForDisplayingCachedVersion()
    {
        self::requireNeededFiles();
    }

    # legend position constants
    const LEGEND_BOTTOM = "bottom";
    const LEGEND_RIGHT = "right";
    const LEGEND_INSET = "inset";
    const LEGEND_NONE = "none";

    # ---- PRIVATE INTERFACE --------------------------------------------------

    /**
    * Include necessary JS/CSS for chart generation.
    */
    protected static function requireNeededFiles()
    {
        $GLOBALS["AF"]->requireUIFile("d3.js");
        $GLOBALS["AF"]->requireUIFile("c3.js");
        $GLOBALS["AF"]->requireUIFile("c3.css");
        $GLOBALS["AF"]->requireUIFile("Chart_Base.css");
    }

    /**
    * Massage data provided by the user into an appropriate format for
    * plotting and do any necessary tweaks to $this->Chart.Child
    * classes MUST implement this method.
    */
    abstract protected function prepareData();

    /**
    * Get RGB hex color when no color supplied.
    * @param string $DataIndex Index for data for which color will be used.
    * @return string RGB hex color string.
    */
    protected function generateRgbColorString($DataIndex)
    {
        return "#".substr(md5($DataIndex), 0, 6);
    }

    /**
     * Merge an array of settings into $this->Chart.
     * @param array $Data Settings to merge.
     */
    protected function addToChart($Data)
    {
        $this->addToArray($this->Chart, $Data);
    }

    /**
     * Merge elements from a source array into a dest array.
     * @param array $Tgt Target array.
     * @param array $Data Data to be added.
     */
    protected function addToArray(&$Tgt, $Data)
    {
        foreach ($Data as $Key => $Val) {
            if (isset($Tgt[$Key]) && is_array($Tgt[$Key]) && is_array($Val)) {
                $this->addToArray($Tgt[$Key], $Val);
            } else {
                $Tgt[$Key] = $Val;
            }
        }
    }

    # data provided by caller
    protected $Data = [];

    # chart parameters that can be changed prior to generation
    protected $LegendPosition = self::LEGEND_BOTTOM;
    protected $Colors = false;
    protected $Labels = [];
    protected $LegendLabels = [];
    protected $Height = 600;
    protected $Width = 600;

    # internal variables used to generate the chart

    # map legend labels (which C3 uses for item identifiers) to the
    # labels we want in the tooltip
    protected $TooltipLabels = [];
    protected $Chart = null;

    # additional javascript to add
    protected $StateVariableJSCode = "";
    protected $HelperFunctionJSCode = "";
    protected $PostChartInitJSCode = "";
}
